package wena

import (
	"context"
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"text/template"
	"time"

	_ "embed"

	"gitee.com/go-errors/errors"
	"gitee.com/go-wena/proc"
	"gitee.com/go-wena/wena/internal/builder"
	"gitee.com/go-wena/wena/internal/git"
	"gitee.com/go-wena/wena/internal/mod"
	"gitee.com/go-wena/wena/internal/parser"
	"gitee.com/go-wena/wena/internal/run"
	"gitee.com/go-wena/wena/internal/watch"
	"gitee.com/go-wena/wena/utils/pathx"
	"github.com/segmentio/go-snakecase"
)

//go:embed main.gogo
var mainTemplate string

//go:embed wena.ignore
var defaultIgnore []byte

type Engine struct {
	Project
	rootPath string
	wenaDIR  string
	srcMod   *mod.File
	builder  *builder.Builder
	cur      *proc.Cmd
}

func New(srcDIR string, builder *builder.Builder) (*Engine, error) {
	rootPath := pathx.FindPathHasChild("go.mod", false, srcDIR)
	if rootPath == "" {
		return nil, errors.Errorf("dir: %s is not in go mod path", srcDIR)
	}

	proj, err := loadProject(filepath.Join(rootPath, projectFileName))
	if err != nil {
		return nil, err
	}

	srcMod, err := mod.Open(rootPath)
	if err != nil {
		return nil, errors.Wrap(err)
	}

	if srcMod.IsEmpty() {
		return nil, errors.Errorf("dir: %s/go.mod is not has a package define", rootPath)
	}

	return &Engine{
		Project:  proj,
		rootPath: rootPath,
		wenaDIR:  filepath.Join(rootPath, ".wena"),
		srcMod:   srcMod,
		builder:  builder,
	}, nil
}

// Watch 生成并监听工作目录
func (e *Engine) Watch(ctx context.Context, br bool) error {
	w, err := watch.New(e.rootPath, defaultIgnore)
	if err != nil {
		return err
	}

	doRun := func() {
		if err := e.Build(ctx, br); err != nil {
			log.Printf("%+v", err)
		}
	}

	var cancel context.CancelFunc = func() {}
	w.SetOnChanged(func(ev watch.Event) {
		name, _ := filepath.Rel(e.rootPath, ev.Name)
		if name == projectFileName {
			log.Printf("%q: %s: loadProject", name, ev.Op.String())
			e.loadProject()
			return
		}
		log.Printf("%q: %s: restart", name, ev.Op.String())
		cancel()
		go func() { cancel = run.Delay(ctx, doRun, time.Second*3) }()
	})

	go doRun()

	return w.Watch(ctx)
}

// Build 生成, run 指示是否生成后运行
func (e *Engine) Build(ctx context.Context, br bool) error {
	defer e.autoClean(ctx)
	return run.Proper(ctx, e.pipes(br)...)
}

// Clean 清理wena临时目录
func (e *Engine) Clean(ctx context.Context) error {
	return run.Proper(ctx, e.clean,
		func(ctx context.Context) error {
			return e.exec(ctx, proc.Command("go", "clean").Name("clean").DIR(e.rootPath))
		},
	)
}

func (e *Engine) autoClean(ctx context.Context) {
	if !e.Keep {
		run.Run(ctx, e.Clean)
	}
}

func (e *Engine) clean(context.Context) error {
	if e.wenaDIR == "" {
		return errors.New("build dir is not generated")
	}
	return os.RemoveAll(e.wenaDIR)
}

func (e *Engine) generate(ctx context.Context) error {
	if ctx.Err() != nil {
		return errors.Wrap(ctx.Err())
	}

	if e.wenaDIR == "" {
		return errors.New("build dir is not generated")
	}

	if err := os.MkdirAll(e.wenaDIR, 0755); err != nil {
		return errors.Wrap(err)
	}

	prj, err := parser.Parse(e.srcMod)
	if err != nil {
		return errors.Wrap(err)
	}

	mainFw, err := os.Create(filepath.Join(e.wenaDIR, "main.go"))
	if err != nil {
		return errors.Wrap(err)
	}
	defer closeIt(mainFw)

	if err := generate(mainFw, prj); err != nil {
		return errors.Wrap(err)
	}

	return mod.New(filepath.Join(e.wenaDIR, "go.mod"), func(tx *mod.File) (err error) {
		if err = tx.AddModuleStmt(filepath.Base(prj.Mod)); err != nil {
			return errors.Wrap(err)
		}

		ver := strings.TrimPrefix(runtime.Version(), "go")
		ver = strings.Join(strings.Split(ver, ".")[:2], ".")
		if err = tx.AddGoStmt(ver); err != nil {
			return errors.Wrap(err)
		}

		if err = tx.AddReplace(prj.Mod, "", prj.Root, ""); err != nil {
			return errors.Wrap(err)
		}

		for _, repl := range e.srcMod.Replace {
			if strings.HasPrefix(repl.New.Path, ".") {
				repl.New.Path, _ = filepath.Abs(repl.New.Path)
			}

			if err = tx.AddReplace(repl.Old.Path, repl.Old.Version, repl.New.Path, repl.New.Version); err != nil {
				return errors.Wrap(err)
			}
		}

		if err = tx.AddRequire(prj.Mod, "v0.0.0-00010101000000-000000000000"); err != nil {
			return errors.Wrap(err)
		}

		if err = tx.AddRequire("gitee.com/go-wena/app", e.Depend); err != nil {
			return errors.Wrap(err)
		}

		for _, req := range e.srcMod.Require {
			if err = tx.AddRequire(req.Mod.Path, req.Mod.Version); err != nil {
				return errors.Wrap(err)
			}
		}

		return nil
	})
}

func (e *Engine) exec(ctx context.Context, c *proc.Cmd) error {
	if e.cur != nil {
		if err := e.cur.Stop(); err != nil {
			log.Printf("kill error: %v\n", err)
		}
	}

	if e.cur != nil {
		_ = e.cur.Wait()
	}

	c.ReadOut(proc.PrintLine("OUT"))
	c.ReadErr(proc.PrintLine("ERR"))
	c.Context(ctx).Verbose(e.builder.Verbose)

	defer func() { e.cur = nil }()
	return c.Start()
}

func (e *Engine) modTidy(ctx context.Context) error {
	return e.exec(ctx, proc.Command("go", "mod", "tidy").Name("ModTidy").DIR(e.wenaDIR))
}

func (e *Engine) build(br bool) run.ProperFunc {
	binary := e.builder.Output
	{
		if binary != "" {
			binary, _ = filepath.Abs(binary)
		}

		if binary == "" {
			if br {
				binary = filepath.Join(e.rootPath, ".wena", e.srcMod.Name())
			} else {
				if e.Output != "" {
					binary, _ = filepath.Abs(e.Output)
				}
				if binary == "" {
					binary = filepath.Join(e.rootPath, e.srcMod.Name())
				}
			}
		}

		if binary != "" && runtime.GOOS == "windows" && !strings.HasSuffix(binary, ".exe") {
			binary += ".exe"
		}
	}

	//inject build info
	ldFlags := []string{e.builder.LDFlags}

	//version
	{
		describe, _ := git.GetDescribe(e.rootPath)
		if e.Version != "" {
			describe.Tag = strings.ReplaceAll(strings.TrimSpace(e.Version), " ", "-")
		}
		log.Println("version:", describe.String())
		ldFlags = append(ldFlags, fmt.Sprintf("-X 'main.version=%s'", describe.Version("v")))
	}

	//name
	{
		var name string
		if e.Name != "" {
			name = strings.ReplaceAll(strings.TrimSpace(e.Name), " ", "_")
		} else {
			name = e.srcMod.Name()
		}
		ldFlags = append(ldFlags, fmt.Sprintf("-X 'main.name=%s'", name))
	}

	//description
	{
		if e.Description != "" {
			description := strings.ReplaceAll(strings.TrimSpace(e.Description), " ", ",")
			ldFlags = append(ldFlags, fmt.Sprintf("-X 'main.description=%s'", description))
		}
	}

	e.builder.LDFlags = strings.Join(ldFlags, " ")

	return func(ctx context.Context) error {
		err := e.exec(ctx, proc.Command("go", e.builder.GenerateArgs(binary)...).Name("BUILD").DIR(e.wenaDIR))
		if err != nil {
			return err
		}
		if br {
			return e.exec(ctx, proc.Command(binary).Name("RUN").DIR(e.rootPath))
		} else if e.builder.UpxPack {
			return e.exec(ctx, proc.Command("upx", "-q", binary).Name("UPX"))
		} else {
			return nil
		}
	}
}

func (e *Engine) pipes(br bool) []run.ProperFunc {
	return []run.ProperFunc{e.clean, e.generate, e.modTidy, e.build(br)}
}

func (e *Engine) loadProject() {
	proj, err := loadProject(filepath.Join(e.rootPath, projectFileName))
	if err != nil {
		log.Printf("load %s: %v", projectFileName, err)
		return
	}
	e.Project = proj
}

func closeIt(closer io.Closer) {
	if closer != nil {
		if err := closer.Close(); err != nil {
			log.Printf("%v", err)
		}
	}
}

func generate(w io.Writer, prj *parser.Project) error {
	gon := func(s string) string {
		n := regexp.MustCompile(`[^\w]+`).ReplaceAllString(snakecase.Snakecase(s), "_")
		n = regexp.MustCompile(`_[a-zA-Z]`).ReplaceAllStringFunc(n, func(s string) string {
			return strings.ToUpper(s[1:])
		})
		return strings.Title(n)
	}

	funcMap := template.FuncMap{
		"title":    strings.Title,
		"trim":     strings.TrimSpace,
		"lower":    strings.ToLower,
		"upper":    strings.ToUpper,
		"contains": strings.Contains,
		"snake":    snakecase.Snakecase,
		"gon":      gon,
		"base":     filepath.Base,
		"unix_now": func() string { return strconv.FormatInt(time.Now().Unix(), 10) },
	}

	return template.Must(template.New("").Funcs(funcMap).Parse(mainTemplate)).Execute(w, prj)
}
